package com.jeequan.components.http;

import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.*;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author roy
 */
@Slf4j
@Configuration
@EnableConfigurationProperties({HttpProperties.class})
public class RestTemplateConfiguration {

    private static final String HTTP = "http";

    private static final String HTTPS = "https";

    private static final String TLS = "TLS";

    private final String[] TLS_PROTOCOL = new String[]{"TLSv1", "TLSv1.1", "TLSv1.2"};

    private SSLConnectionSocketFactory getSSLConnection(HttpProperties httpProperties) throws KeyStoreException, NoSuchAlgorithmException, IOException, UnrecoverableKeyException, KeyManagementException, CertificateException {
        SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
        SSLContext sslContext = null;
        // 是否信任所有站点
        if (httpProperties.isTrustAllSite()) {
            log.info("RestTemplate远程调用，将信任所有站点，忽略证书合法性校验");
            sslContextBuilder.loadTrustMaterial(null, new TrustAllStrategy());
            sslContext = sslContextBuilder.build();
        } else {
            initTrustStore(httpProperties.getTrustCertPath());
            sslContext = SSLContext.getDefault();
        }

        SSLConnectionSocketFactory sslConnectionSocketFactory = null;
        if (httpProperties.isHostVerifier()) {
            log.info("RestTemplate远程调用，将校验请求域名与证书域名的一致性");
            sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, TLS_PROTOCOL, null, new MyHostnameVerifier());
        } else {
            log.info("RestTemplate远程调用，证书域名校验已被忽略，生产环境不建议关闭域名校验( host-verifier=false )");
            sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
        }

        return sslConnectionSocketFactory;
    }

    @Bean
    public RestTemplate restTemplate(HttpProperties httpProperties) throws CertificateException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException {
        log.info("RestTemplate初始化... httpProperties参数：{}", httpProperties.toString());

        SSLConnectionSocketFactory sslConnectionSocketFactory = getSSLConnection(httpProperties);

        Registry<ConnectionSocketFactory> connectionSocketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register(HTTP, PlainConnectionSocketFactory.INSTANCE)
                .register(HTTPS, sslConnectionSocketFactory)
                .build();

        // 连接池管理器
        PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(connectionSocketFactoryRegistry);
        // 总连接数
        pollingConnectionManager.setMaxTotal(httpProperties.getMaxTotal());
        // 单一路由最大连接数
        pollingConnectionManager.setDefaultMaxPerRoute(httpProperties.getMaxPerRoute());

        // 连接存活策略
        ConnectionKeepAliveStrategy connectionKeepAliveStrategy = (httpResponse, httpContext) -> httpProperties.getKeepAliveTimeout() * 1000;

        // 请求超时参数配置
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(httpProperties.getConnectTimeout())
                .setConnectionRequestTimeout(httpProperties.getConnectionRequestTimeout())
                .setSocketTimeout(httpProperties.getSocketTimeout())
                .build();

        // 重试策略
        HttpRequestRetryHandler requestRetryHandler = getRetryHandler(httpProperties);

        HttpClient httpClient = HttpClients.custom()
                .setConnectionManager(pollingConnectionManager)
                .setDefaultRequestConfig(requestConfig)
                .setRetryHandler(requestRetryHandler)
                .setKeepAliveStrategy(connectionKeepAliveStrategy)
                .evictIdleConnections(httpProperties.getMaxIdleTime(), TimeUnit.SECONDS)
                .evictExpiredConnections()
                .build();

        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);

        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        messageConverters.add(new FormHttpMessageConverter());
        messageConverters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
        messageConverters.add(new MappingJackson2HttpMessageConverter());
        messageConverters.add(new ResourceHttpMessageConverter());
        messageConverters.add(new FastJsonHttpMessageConverter());
        restTemplate.setMessageConverters(messageConverters);

        StringBuilder sb = new StringBuilder("\n");
        sb.append("===================================================================\n");
        sb.append("RestTemplate初始化完成\n");
        sb.append("===================================================================\n");

        log.info(sb.toString());

        return restTemplate;
    }

    /**
     * 重试策略
     *
     * @param httpProperties
     * @return
     */
    private HttpRequestRetryHandler getRetryHandler(HttpProperties httpProperties) {
        return (exception, executionCount, context) -> {
            if (executionCount > httpProperties.getFailRetry()) {
                // 如果执行次数，大于最大重试次数，说明已重试结束
                return false;
            }
            if (exception instanceof SSLHandshakeException || exception instanceof UnknownHostException) {
                // 如果是握手异常，或未知主机名，不发起重试
                return false;
            }
            if (exception instanceof InterruptedIOException || exception instanceof NoHttpResponseException) {
                // 如果异常原因是网络IO中断，或者目标服务断开连接引起的
                log.warn("RestTemplate 发起Http请求重试. 重试的原因：{}", exception.getMessage());
                return true;
            }

            HttpClientContext clientContext = HttpClientContext.adapt(context);
            HttpRequest httpRequest = clientContext.getRequest();
            // HttpEntityEnclosingRequest 类型的请求代表有request body的请求，如果：POST,PUT,PATCH
            boolean idempotent = !(httpRequest instanceof HttpEntityEnclosingRequest);
            if (idempotent) {
                // 如果是幂等请求，则可以发起重试
                log.warn("RestTemplate 发起Http请求重试. 重试的原因：{}", exception.getMessage());
                return true;
            }
            // 如果以上条件均不符合，默认不发起重试
            return false;
        };
    }


    /**
     * 初始化证书信任库，将自签名信任库和JRE默认信任库合并
     *
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     * @throws KeyStoreException
     * @throws CertificateException
     * @throws IOException
     */
    public void initTrustStore(String certFilePath) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException,
            CertificateException, IOException {

        // 合并后的证书管理器
        X509TrustManager mergedTrustManager = null;

        // JRE自带第三方权威CA机构根证书管理器
        X509TrustManager jreTrustManager = getJreTrustManager();

        // 自签名证书管理器
        X509TrustManager myTrustManager = getMyTrustManager(certFilePath);

        if (myTrustManager == null) {
            mergedTrustManager = jreTrustManager;
        } else {
            mergedTrustManager = createMergedTrustManager(jreTrustManager, myTrustManager);
        }

        // 初始化默认的：SSLContext
        setDefaultSSLContext(mergedTrustManager);
    }

    /**
     * 获取JRE自带的CA证书信任库
     *
     * @return
     * @throws NoSuchAlgorithmException
     * @throws KeyStoreException
     */
    private X509TrustManager getJreTrustManager() throws NoSuchAlgorithmException, KeyStoreException {
        return findDefaultTrustManager(null);
    }

    /**
     * 获取自签名证书信任库
     *
     * @param certFilePath
     * @return
     * @throws KeyStoreException
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws CertificateException
     */
    private X509TrustManager getMyTrustManager(String certFilePath) throws KeyStoreException, IOException,
            NoSuchAlgorithmException, CertificateException {

        if (StringUtils.isEmpty(certFilePath)) {
            log.info("自签名证书文件路径未配置： easy.http.connection.trust-cert-path is empty.");
            return null;
        }

        KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        // 初始化一个空的证书信任库
        myTrustStore.load(null, null);

        log.info("加载自签名CA证书：{}", certFilePath);
        try (FileInputStream certInputStream = new FileInputStream(certFilePath)) {
            try (BufferedInputStream bis = new BufferedInputStream(certInputStream)) {
                X509Certificate caCert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(bis);
                log.debug(caCert.toString());
                // 将自签名证书加载到信任库中
                myTrustStore.setCertificateEntry("myself-ca", caCert);
            }
        }
        return findDefaultTrustManager(myTrustStore);
    }

    /**
     * 查找JRE默认信任库
     *
     * @param keyStore
     * @return
     * @throws NoSuchAlgorithmException
     * @throws KeyStoreException
     */
    private X509TrustManager findDefaultTrustManager(KeyStore keyStore)
            throws NoSuchAlgorithmException, KeyStoreException {
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        // If keyStore is null, tmf will be initialized with the default trust store
        tmf.init(keyStore);

        for (TrustManager tm : tmf.getTrustManagers()) {
            if (tm instanceof X509TrustManager) {
                return (X509TrustManager) tm;
            }
        }
        return null;
    }


    /**
     * 将2个证书信任库合并
     *
     * @param jreTrustManager
     * @param customTrustManager
     * @return
     */
    private X509TrustManager createMergedTrustManager(X509TrustManager jreTrustManager,
                                                      X509TrustManager customTrustManager) {
        return new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                // If you're planning to use client-cert auth,
                // merge results from"defaultTm" and"myTm".
                return jreTrustManager.getAcceptedIssuers();
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                try {
                    customTrustManager.checkServerTrusted(chain, authType);
                } catch (CertificateException e) {
                    // This will throw another CertificateException if this fails too.
                    jreTrustManager.checkServerTrusted(chain, authType);
                }
            }

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                // If you're planning to use client-cert auth,
                // do the same as checking the server.
                jreTrustManager.checkClientTrusted(chain, authType);
            }

        };
    }


    /**
     * 设置SSLContext 默认实例
     *
     * @param mergedTrustManager
     * @throws NoSuchAlgorithmException
     * @throws KeyManagementException
     */
    private void setDefaultSSLContext(X509TrustManager mergedTrustManager)
            throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance(TLS);
        sslContext.init(null, new TrustManager[]{mergedTrustManager}, null);

        // You don't have to set this as the default context,
        // it depends on the library you're using.
        SSLContext.setDefault(sslContext);
    }

}
